package ms.jasim.framework;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;

import ms.jasim.framework.SolutionSimulator.SimulatorArg;
import ms.jasim.model.Evaluator;
import ms.jasim.model.JasimModel;
import ms.jasim.model.event.Event;
import ms.jasim.model.event.EventSet;
import ms.jasim.pddl.PddlPlanner;
import ms.jasim.pddl.PddlSolution;
import ms.jasim.pddl.PddlSolution.PddlAction;
import ms.utils.IEvent;
import ms.utils.INamedItem;
import ms.utils.INamedList;
import ms.utils.JasimUtils;
import ms.utils.NamedList;

import org.eclipse.emf.common.util.URI;

public class EventSimulator implements Runnable {
	
	private PddlSolution solution;
	private SimulatedTreeNode root;
	private ArrayList<Event> eventList;
	private NamedList<EventHistory> history;
	private Queue<SimulatedTreeNode> queue;
	
	private int progressTotal;
	private int progressValue;
	private boolean cancelled;
	private int maxBranches = 0;
	private int maxCpuTime = 0;
	private String modelFilename;
	private SimulatedTreeNode activeNode;
	private IJasimAppContext context;
	private IMessageConsole stdout;
	
	public static class EventHistory implements INamedItem {

		private Event event;
		private int time;
		private SimulatedTreeNode node;

		public EventHistory(Event event, int time, SimulatedTreeNode node) {
			this.event = event;
			this.time = time;
			this.node = node;
		}

		public Event getEvent() {
			return event;
		}

		public int getTime() {
			return time;
		}
	
		@Override
		public String getName() {
			return event != null ? event.getName() : null;
		}

		public SimulatedTreeNode getNode() {
			return node;
		}
	
		@Override
		public String toString() {
			return String.format("%s at time %d", getName(), time);
		}
	}

	public class SimulatedTreeNode implements IEvent<SimulatorArg> {

		private PddlSolution solution;
		private SolutionSimulator simulator;
		private int startTime;
		private int endTime;
		private ArrayList<SimulatedTreeNode> children;
		private String name;
		private PddlPlanner generator;
		private JasimModel model;
		private EventSimulationInfoProviderImpl infoProvider;
		private IJasimAppContext evContext;
		private SimulatedTreeNode parent;
		private HashMap<Evaluator, EvaluatorResult[]> evalResultSet;

		public class EventSimulationInfoProviderImpl implements IEventSimulationInfoProvider {
			private SimulatorArg arg;

			@Override
			public SimulatorArg getSimulatorArg() {
				return this.arg;
			}
			
			public void setSimulatorArg(SimulatorArg arg) {
				this.arg = arg;
			}
			
			@Override
			public SolutionSimulator getSimulator() {
				return simulator;
			}
			
			@Override
			public INamedList<EventHistory> getEventHistory() {
				return history;
			}
		}
		
		public SimulatedTreeNode(PddlSolution solution, int startTime) {
			this.solution = solution;
			this.startTime = startTime;
			this.name = solution.getName();
		}
		
		/**
		 * Implementation of IEvent &lt;SimulatorArg&gt;
		 * <p> Do not call directly. 
		 */
		@Override
		public void run(Object sender, final SimulatorArg arg) {
			ArrayList<Event> triggerEvent = null;
			boolean replan = false;
			
			// browse over event instances to see which one is
			// triggered?
			infoProvider.setSimulatorArg(arg);
			
			for (Event event: eventList)
				if (event.canTriggerNow(evContext, arg)) {
					if (triggerEvent == null)
						triggerEvent = new ArrayList<Event>();
					triggerEvent.add(event);
				}
			
			if (triggerEvent != null) {
				
				for (Event event : triggerEvent) {
					history.add(new EventHistory(event, arg.getTime(), this));
					writeMessage("--->Event %s is triggered at time %d in solution %s.\n", event.getName(), arg.getTime(), name);
					if (event.react(evContext, model))
						replan = true;
					eventList.remove(event);
				}
				
				if (replan) {
					// reflect current situation to the model
					for (PddlAction act : arg.getSimulator().getFinishedActions())
						model.reflectFinishedAction(act);
					arg.setCancel(true);
				}
			}
		}

		public void run() {
			
			writeMessage("Visit node %s\r\n", solution.getName());
			model =  JasimUtils.cloneModel(solution.getProblem().getModel());
			
			evContext = context.createChildContext("");
			
			evContext.addService(this.infoProvider = new EventSimulationInfoProviderImpl());
			evContext.addService(model);
			
			this.simulator = new SolutionSimulator(evContext, model, solution);
			simulator.addListener(this);
			this.endTime = simulator.simulate(startTime, 9999);
			
			if (maxBranches > 0 && !simulator.isCompleted()) {
				NamedList<PddlSolution> sols = getBranches();
				if (sols != null) {
					this.children = new ArrayList<SimulatedTreeNode>();
					for (int j = 0; j < sols.size() && sols.get(j).Actions().size() > 0; j++) {
						SimulatedTreeNode child = new SimulatedTreeNode(sols.get(j), endTime - 1);
						child.setName(name + "->" + j);
						child.parent = this;
						sols.get(j).setName(name + "->" + j);
						children.add(child);
					}
				}
			}
			
			writeMessage(simulator.convertTimeSlotsToString() + "\r\n");
			
			if (!simulator.isCompleted())
				if (children.size() == 0)
					writeMessage("--> dangling node detected\r\n");
				else
					writeMessage("--> replan node\r\n");
			else 
				writeMessage("--> leaf node\r\n");
		}

		private void setName(String value) {
			this.name = value;
			
		}

		private NamedList<PddlSolution> getBranches() {
			try {
				this.generator = new PddlPlanner(context);
				generator.setPddlModel(modelFilename, model);
				generator.setMaxSolution(maxBranches);
				generator.setMaxCpuTime(maxCpuTime);
				generator.run();
				NamedList<PddlSolution> result = generator.getSolutions();
				generator = null;
				return result;
			} catch (Exception e) {
				return null;
			}
		}

		public void cancel() {
			if (generator != null)
				generator.cancel();
		}
		
		public List<SimulatedTreeNode> getChildren() {
			return children;
		}

		public SimulatedTreeNode getParent() {
			return this.parent;
		}

		public PddlSolution getSolution() {
			return solution;
		}
		
		public String getLabel() {
			return solution.getName();
		}

		public SolutionSimulator getSimulator() {
			return simulator;
		}
	
		public NamedList<EventHistory> getEventHistory() {
			/*NamedList<EventHistory> result = new NamedList<EventHistory>();
			for (EventHistory event: history)
				if (event.getNode() == this)
					result.add(event);
			return result;*/
			return history;
		}

		public int getDepth() {
			if (this.getParent() == null)
				return 0;
			return getParent().getDepth() + 1;
		}

		public EvaluatorResult getEvaluatorResult(Evaluator evaluatorInfo, boolean finishedActionOnly) {
			EvaluatorResult[] res = null;
			
			if (this.evalResultSet == null)
				this.evalResultSet = new HashMap<Evaluator, EvaluatorResult[]>();
				
			if (!evalResultSet.containsKey(evaluatorInfo)) {
				res = new EvaluatorResult[2];
				evalResultSet.put(evaluatorInfo, res);
				
				IEvaluator instance = evaluatorInfo.getInstance();
				if (instance != null) {
					res[0] = instance.evaluate(context, model, evaluatorInfo, getSolution().Actions());
					res[1] = instance.evaluate(context, model, evaluatorInfo, simulator.getFinishedActions());
					
					if (getParent() != null) {
						EvaluatorResult parentRes = getParent().getEvaluatorResult(evaluatorInfo, true); 
						res[0] = instance.combine(res[0], parentRes);
						res[1] = instance.combine(res[1], parentRes);
					}
				}
			}
			else res = evalResultSet.get(evaluatorInfo);
			return res != null ? res[finishedActionOnly? 1: 0] : null;
		}

	
		public boolean isLeaf() {
			return children == null;
		}
	}
	
	public EventSimulator(IJasimAppContext context, PddlSolution solution, List<EventSet> eventSets) {
		this.solution = solution;
		this.context = context;
		this.eventList = new ArrayList<Event>();
		this.stdout = context.getService(IMessageConsole.class);
		this.history = new NamedList<EventHistory>();
		
		IUriProvider uriProvider = context.getService(IUriProvider.class);
		JasimModel model = solution.getProblem().getModel();
		URI uri = uriProvider != null ? uriProvider.getUri(model) : model.getURI();
		
		if (uri != null)
			modelFilename = uri.toFileString();
		
		for (EventSet set: eventSets) 
			if (set.isEnabled()) {
				maxBranches = Math.max(maxBranches, set.getMaxBranches());
				maxCpuTime = Math.max(maxCpuTime, set.getMaxCpuTime());
				for (Event event: set.getEvent())
					if (event.isEnabled())
						eventList.add(event);
			}
	}

	@Override
	public void run() {
		writeMessage("Start simulating solution: %s.\r\n\r\n", solution.getName());
		root = new SimulatedTreeNode(solution, 0);
		this.queue = new LinkedList<SimulatedTreeNode>();
		queue.add(root);
		
		progressTotal = 1;
		progressValue = 0;
		cancelled = false;
		
		while (!isCancelled() && !queue.isEmpty()) {
			activeNode = queue.poll();
			activeNode.run();
			if (activeNode.children != null) {
				progressTotal += activeNode.children.size();
				for (SimulatedTreeNode child: activeNode.children)
					queue.add(child);
			}
			progressValue++;
		}
		
		this.queue = null;
		writeMessage("\r\nSimulation stop at %s.\r\n", new Date());
	}

	public void setMaxBranches(int value) {
		this.maxBranches = value;
	}
	
	public void setMaxCpuTime(int value) {
		this.maxCpuTime = value;
	}
	
	public boolean isCancelled() {
		return this.cancelled;
	}
	
	public synchronized void cancel() {
		this.cancelled = true;
		if (activeNode != null)
			activeNode.cancel();
	}

	public SimulatedTreeNode getRootNode() {
		return root;
	}
	
	private void writeMessage(String template, Object...arg) {
		String txt = String.format(template, arg);
		if (stdout != null)
			stdout.write("Simulation", txt);
		else
			System.out.println(txt);
			
	}

	
	public PddlSolution getSolution() {
		return this.solution;
	}
}
